﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Reflection;
using System.Diagnostics;


namespace Engine.Dtd.Schema {
    /// <summary>
    /// Represents the document type definition that is a set of 
    /// markup declarations that define a document type for SGML family markup languages.
    /// It use a terse formal syntax that declares precisely which elements and references may appear
    /// where in the document of the particular type, and what the elements contents and attributes are.
    /// Also declare internal and external entities which may be used in the instance document.
    /// </summary>
    [DebuggerDisplay("{Name}")]
    public sealed class DtdSchema {
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Dictionary<DName, DElementDeclaration> elements;
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Dictionary<string, DEntity> entities;
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Dictionary<string, DNotation> notations;
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private ICollection<DName> blockElements;
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private ICollection<DName> inlineElements;
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private DNameCollection nameCollection;
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private static object lockObject = new object();

        /// <summary>
        /// Gets the name of the root element of a document.
        /// </summary>
        public DName Name { get; private set; }
        /// <summary>
        /// Gets the declaration for the root element of a document.
        /// </summary>
        public DElementDeclaration RootDeclaration { get; private set; }
        /// <summary>
        /// Gets the base URL of the DTD schema.
        /// </summary>
        public Uri BaseUrl { get; private set; }

        private DtdSchema() {
            this.elements = new Dictionary<DName, DElementDeclaration>();
            this.entities = new Dictionary<string, DEntity>();
            this.notations = new Dictionary<string, DNotation>();
            this.blockElements = DNameCollection.EmptySequence;
            this.inlineElements = DNameCollection.EmptySequence;
            this.nameCollection = new DNameCollection();
            }

        private DtdSchema(Uri baseUrl)
            : this() {
            BaseUrl = baseUrl;
            }

        private static DtdSchema Load(TextReader stream, Uri address, string name, string subset) {
            Guard.ArgumentNotNull(stream, "stream");
            Guard.ArgumentNotNull(address, "address");
            Guard.ArgumentNotNullOrWhitespace(name, "name");
            lock (lockObject) {
                Entity entity = new Entity(name, address, stream);
                if (!string.IsNullOrEmpty(subset)) {
                    Entity subsetEntity = new Entity(name, subset);
                    subsetEntity.Open(entity);
                    entity = subsetEntity;
                    }
                // Parse
                DtdSchema schema = new DtdSchema(address);
                DtdReader reader = new DtdReader(entity, schema.nameCollection, new EntityResolver { BaseUrl = address });
                schema.Parse(reader);
                // Initialize element declaration
                foreach (DElementDeclaration declaration in schema.elements.Values) {
                    declaration.ElementType = schema.GetElementType(declaration.Name);
                    foreach (DElement element in declaration.ContentModel.Groups) {
                        element.SetDeclaration(schema.GetElementDeclaration(element.Name));
                        }
                    }
                // Initialize inclusion exceptions
                foreach (DElementDeclaration declaration in schema.elements.Values)
                    declaration.ContentModel.InitializeInclusionExceptions();

                // Initialize the root declaration
                DElementDeclaration root = null;
                DName rootName = schema.nameCollection[name];
                if (rootName == null) {
                    rootName = schema.nameCollection[name.ToLowerInvariant()];
                    if (rootName == null)
                        rootName = schema.nameCollection[name.ToUpperInvariant()];
                    }
                if (rootName != null && schema.elements.TryGetValue(rootName, out root)) {
                    schema.Name = root.Name;
                    schema.RootDeclaration = root;
                    }
                else
                    throw new DtdException(string.Format("The root element '{0}' could not be found in the DTD schema.", name));
      
                return schema;
                }
            }

        private DElementType GetElementType(DName name) {
            if (BlockElements.Contains(name))
                return DElementType.Block;
            else if (InlineElements.Contains(name))
                return DElementType.Inline;
            else if (object.ReferenceEquals(DNameCollection.EmptySequence, BlockElements) ||
                object.ReferenceEquals(DNameCollection.EmptySequence, InlineElements))
                return DElementType.Unknown;
            else
                return DElementType.None;
            }

        private static DtdSchema Load(Uri absoluteUrl, string name, string subset) {
            UrlResolver resolver = new UrlResolver();
            using (TextReader stream = resolver.GetEntity(absoluteUrl)) {
                return Load(stream, absoluteUrl, name, subset);
                }
            }

        /// <summary>
        /// Creates a new DTD schema from a TextReader.
        /// </summary>
        /// <param name="stream">The reader to load the DTD from.</param>
        /// <param name="address">The base address of this document.</param>
        /// <param name="name">The name of the root element of the document.</param>
        /// <exception cref="Engine.Dtd.DtdException">DtdException</exception>
        /// <returns>A <see cref="DtdSchema"/> instance that encapsulates the DTD.</returns>
        public static DtdSchema Load(TextReader stream, Uri address, string name) {
            return Load(stream, address, name, null);
            }

        /// <summary>
        /// Creates a new DTD schema form an unique resource identifier.
        /// </summary>
        /// <param name="absoluteUri">A URL that references to the file of web resource to load into DTD schema.</param>
        /// <param name="name">The name of the root element of the document.</param>
        /// <returns>A <see cref="DtdSchema"/> instance that encapsulates the DTD.</returns>
        public static DtdSchema Load(Uri absoluteUri, string name) {
            return Load(absoluteUri, name, null);
            }

        /// <summary>
        /// Gets a collection of the entities contained in the schema.
        /// </summary>
        public IEnumerable<DEntity> Entities {
            get { return entities.Values; }
            }

        /// <summary>
        /// Gets a collection of the element declaration contained in the schema.
        /// </summary>
        public IEnumerable<DElementDeclaration> ElementDeclarations {
            get { return elements.Values; }
            }

        /// <summary>
        /// Gets a collection of the notations contained in the schema.
        /// </summary>
        public IEnumerable<DNotation> Notations {
            get { return notations.Values; }
            }

        /// <summary>
        /// Finds an element declaration in the DTD schema with the specified name.
        /// </summary>
        /// <remarks>
        /// The <paramref name="name"/> is case sensitive.
        /// </remarks>
        /// <param name="name">The name of the <see cref="Engine.Dtd.Schema.DElementDeclaration"/> to find and return.</param>
        /// <returns>The <see cref="Engine.Dtd.Schema.DElementDeclaration"/> matching the specified name if exist, otherwise null.</returns>
        public DElementDeclaration GetElementDeclaration(string name) {
            if (name == null)
                return null;
            DName n = nameCollection[name];
            if (n == null)
                return null;
            DElementDeclaration declaration;
            elements.TryGetValue(n, out declaration);
            return declaration;
            }

        /// <summary>
        /// Gets an entity in the DTD schema with the specified name.
        /// </summary>
        /// <param name="name">The name of the <see cref="DEntity"/> to find.</param>
        /// <returns>The specified DEntity from the DTD.</returns>
        public DEntity GetEntity(string name) {
            if (name == null)
                return null;
            DEntity entity;
            this.entities.TryGetValue(name, out entity);
            return entity;
            }

        internal IEnumerable<DName> BlockElements {
            get { return blockElements; }
            }

        internal IEnumerable<DName> InlineElements {
            get { return inlineElements; }
            }

        internal DNameCollection Names {
            get { return nameCollection; }
            }

        private void Parse(DtdReader reader) {
            while (reader.Read()) {
                switch (reader.ReadState) {
                    case DtdReaderState.ReadComment:
                        reader.ReadComment();
                        break;
                    case DtdReaderState.ReadElement:
                        foreach (DElementDeclaration element in reader.ReadElementDeclaration())
                            elements.Add(element.Name, element);
                        break;
                    case DtdReaderState.ReadAttributeDeclaration:
                        DAttributeDeclaration declaration = reader.ReadAttributeDeclaration();
                        foreach (DName name in declaration.ElementNames) {
                            DElementDeclaration element;
                            if (elements.TryGetValue(name, out element)) {
                                if (object.ReferenceEquals(element.Attributes, DAttributeDeclaration.EmptySequence))
                                    element.Attributes = declaration.Attributes;
                                else
                                    element.Attributes = element.Attributes.Concat(declaration.Attributes).ToArray();
                                }
                            else
                                throw new DtdException(string.Format("ATTLIST references undefined ELEMENT {0}.", name));
                            }
                        break;
                    case DtdReaderState.ReadGeneralEntity:
                        DEntity entity = reader.ReadEntity();
                        entities.Add(entity.Name, entity);
                        break;
                    case DtdReaderState.ReadParameterEntity:
                        entity = reader.ReadEntity();
                        if (string.Equals(entity.Name, "block", StringComparison.OrdinalIgnoreCase)) {
                            InitializeCollection(ref blockElements);
                            reader.ReadEntityLiteral(entity, ref blockElements);
                            }
                        else if (string.Equals(entity.Name, "inline", StringComparison.OrdinalIgnoreCase)) {
                            InitializeCollection(ref inlineElements);
                            reader.ReadEntityLiteral(entity, ref inlineElements);
                            }
                        break;
                    case DtdReaderState.ReadNotation:
                        DNotation notation = reader.ReadNotation();
                        if (notations.ContainsKey(notation.Name))
                            throw new DtdException(string.Format("A given name '{0}' must not be declared in more than one notation declaration.", notation.Name));
                        notations.Add(notation.Name, notation);
                        break;
                    default:
                        break;
                    }
                }
            }

        private static void InitializeCollection(ref ICollection<DName> collection) {
            if (object.ReferenceEquals(collection, DNameCollection.EmptySequence))
                collection = new List<DName>();
            else
                collection.Clear();
            }
        }
    }
